package it.softphone.commons.service.config;

import static it.softphone.commons.util.LogUtil.logger;

import it.softphone.commons.exception.NullParameterException;
import it.softphone.commons.service.ServiceObjectFactory;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.regex.Pattern;

import org.apache.commons.configuration.CompositeConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.SystemConfiguration;

/**
 * Abstract factory for configuration. 
 * 
 * @author softphone
 *
 */
public abstract class ConfigurationServiceFactory {

	/**
	 * 
	 * 
	 * @param uri
	 * @return true if factory is able to process given uri
	 */
	public boolean acceptUri( java.net.URI uri ) {
		return true;
	}
	
	/**
	 * abstract factory method. This method must be implemented if you have to create a new configuration implementation
	 * 
	 * @param uri configuration source
	 * @return Configuration service
	 * @throws ConfigurationException
	 */
	public abstract Configuration newConfiguration( java.net.URI uri ) throws ConfigurationException;
	
	/**
	 * System variable name that should contain FQN of the service implementation
	 */
	public static final String SERVICE_CONFIGURATION_PROPERTY = "it.softphone.commons.service.config";
	
	/**
	 * Factory method. This method lookup for abstract configuration factory in the following order:
	 * 
	 * Deprecated use getConfigurationService( java.net.URI uri ) instead
	 * 
	 * <pre>
	 * 1. lookup for a SPI implementation 
	 * 2. lookup for system property 
	 * 3. return a default implementation see DefaultFactory
	 * </pre>
	 * 
	 * @param uri configuration source. its format is defined from factory @see DefaultFactory 
	 *            
	 * @param parameters initialization data. It is currently ignored.
	 * @return Configuration service
	 * @throws ConfigurationException
	 * 
	 * @see {@link #getConfigurationService(java.net.URI)}
	 */
	@Deprecated
    public static synchronized Configuration getConfigurationService( String uri, java.util.Map<String,Object> parameters ) throws ConfigurationException  {
		return getConfigurationService( uri );
    } 
    
	/**
	 * Factory method. This method lookup for abstract configuration factory in the following order:
	 * 
	 * 1. lookup for a SPI implementation 
	 * 2. lookup for system property 
	 * 3. return a default implementation see DefaultFactory
	 * 
	 * @param uri configuration source. its format is defined from factory @see DefaultFactory 
	 *            
	 * @return Configuration service
	 * @throws ConfigurationException
	 */    
    public static synchronized Configuration getConfigurationService( String uri ) throws ConfigurationException  {
    	try {
			return getConfigurationService(new java.net.URI(uri));
		} catch (URISyntaxException e) {
			throw new ConfigurationException(e);
		}
    }

    /**
	 * Factory method. This method lookup for abstract configuration factory in the following order:
	 * 
	 * <pre>
	 * 1. lookup for a SPI implementation 
	 * 2. lookup for system property 
	 * 3. return a default implementation see DefaultFactory
	 * </pre>
	 * 
	 * @param uri configuration source. its format is defined from factory @see DefaultFactory 
	 *            
	 * @return Configuration service
	 * @throws ConfigurationException
	 */    
    public static synchronized Configuration getConfigurationService( java.net.URI uri ) throws ConfigurationException  {
    	if(uri==null ) throw new NullParameterException( "uri");
    	
    	ConfigurationServiceFactory factory = null;
    	
    	java.net.URI source = uri;
    	    	
    	try {
    	
    		java.util.List<ConfigurationServiceFactory> factories = ServiceObjectFactory.getSPIServices(ConfigurationServiceFactory.class);
    		
    		if( factories!=null && !factories.isEmpty()) {
    			
    			for( ConfigurationServiceFactory f : factories ) {
    				
    				if( f.acceptUri(source)) {
    					factory = f;
    					break;
    				}
    			}
    		}
    		

		} catch (Exception e) {
			throw new ConfigurationException(e);
		}
		
		if( factory==null ) {
			
			try {
				factory = ServiceObjectFactory.getService(SERVICE_CONFIGURATION_PROPERTY, DefaultFactory.class);
			} catch (Exception e) {
				throw new ConfigurationException(e);
			}
		}
		
		Configuration c = factory.newConfiguration(source);
		return c;
    }
    
    /**
     * Default configuration Factory implementation. It assume that reader is associated a property file and return its contents joined with system properties
     * 
     * <pre>
     * 
     * this configuration factory support the following sources:
     * 
     * - classpath: search property file in classpath (context & system)
     * - file: search property file on file system
     * - url: resolve URL and get property data from it  
     * 
     * Examples:
     * 
     *  final String path = getServletContext().getRealPath("WEB-INF/config.properties");
	 *	
	 *	java.util.Map<String,Object> parameters = new java.util.HashMap<String,Object>(1);
	 *	parameters.put( "config_file", path);
	 *	
	 *	try {
	 *		Configuration conf = ConfigurationServiceFactory.getConfigurationService( "file:${config_file}", parameters );
	 *		
	 *		DatabaseUtils.initFormConfiguration(conf);
	 *		
	 *	} catch (ConfigurationException e1) {
	 *		final String msg = "error while reading configuration WEB-INF/config.properties";
	 *		logger.error( msg , e1 );
	 *		throw new ServletException( msg );
	 *		
	 *	}
	 *
     * <pre>
     * 
     * @author softphone
     *
     */
    public static class DefaultFactory extends ConfigurationServiceFactory {

    	private static final String URI_EXPRESSION = "(classpath|file|http|jar):(.+)";
    	private static java.util.regex.Pattern uriPattern = java.util.regex.Pattern.compile(URI_EXPRESSION, Pattern.CASE_INSENSITIVE);   	


		private Object processUri( java.net.URI uri ) throws ConfigurationException {
        	
			String scheme = uri.getScheme();
			String source = uri.getRawSchemeSpecificPart();
        	
        	
        	java.io.Reader result = null;
        	
        	if( scheme.equalsIgnoreCase("classpath")) {
        		java.io.InputStream is =null;
        		ClassLoader cl = Thread.currentThread().getContextClassLoader();
    			
    			is = cl.getResourceAsStream(source);
    			
    			if( is==null ) {
            		logger.warn( String.format("resource [%s] doesn't exist in context classloader", source ));   
            		
            		cl = getClass().getClassLoader();
            		
        			is = cl.getResourceAsStream(source);
        			
        			if( is==null ) {
                		throw new ConfigurationException( String.format("resource [%s] doesn't exist in classloader", source ));        				
        			}
        			
        		}
        		
        		result = new java.io.InputStreamReader(is);
        		
        	}
        	else if( scheme.equalsIgnoreCase( "factory")) {
        		
        		String factoryClassName = uri.getAuthority();
        		
        		if( factoryClassName==null ) throw new ConfigurationException( "factory class is not specified!" );
        		
        		try {
					Class<?> factoryClass = Class.forName( factoryClassName );
					
					if( !ConfigurationServiceFactory.class.isAssignableFrom(factoryClass)) {
						throw new ConfigurationException( "factory class is not ConfigurationServiceFactory!" );
					}
					
					ConfigurationServiceFactory factory = (ConfigurationServiceFactory) factoryClass.newInstance(); 
					
					Configuration c = factory.newConfiguration(uri);
					
					return c;
					
				} catch (ClassNotFoundException e) {
            		throw new ConfigurationException(e);        				
				} catch (InstantiationException e) {
            		throw new ConfigurationException(e);        				
				} catch (IllegalAccessException e) {
            		throw new ConfigurationException(e);        				
				}
        	}
        	else {
        		try {
        			java.net.URL url = uri.toURL();
        		
 					
					result = new java.io.InputStreamReader(url.openStream());
					
				} catch (MalformedURLException e) {
            		throw new ConfigurationException( String.format("url [%s] is not valid!", source ), e);        				
				} catch (IOException e) {
            		throw new ConfigurationException( String.format("error opening url [%s]!", source ), e);        				
				}
        	}
        	
        	return result;
        }

		@Override
		public boolean acceptUri(java.net.URI uri) {
			return uriPattern.matcher(uri.getScheme()).matches();
		}
        
		@Override
		public Configuration newConfiguration(java.net.URI uri) throws ConfigurationException {
        	
			Object r = processUri( uri );
			
			if( r instanceof java.io.Reader ) {
			
				 
				PropertiesConfiguration conf = new PropertiesConfiguration();
			
				conf.load( (java.io.Reader)r );
			
				CompositeConfiguration result = new CompositeConfiguration();
			
				result.addConfiguration(conf);
				result.addConfiguration( new SystemConfiguration() );
			
				return result;
			} else if( r instanceof Configuration ) {
				return (Configuration) r;
			}

			return null;
		}

    	
    }

}
